package com.xinqi.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xinqi.common.RedisContents;
import com.xinqi.common.SystemContents;
import com.xinqi.dto.Result;
import com.xinqi.entity.Blog;
import com.xinqi.entity.User;
import com.xinqi.mapper.BlogMapper;
import com.xinqi.service.BlogService;
import com.xinqi.service.UserService;
import com.xinqi.util.ThreadPoolUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;
import redis.clients.jedis.*;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements BlogService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public List<Blog> getPageByLimited(Page<Blog> pageParam) {
        Page<Blog> blogPage = baseMapper.selectPage(pageParam, null);
        List<Blog> records = blogPage.getRecords();
        return records;
    }

    @Override
    public Integer queryBlogLikesNumber(Long id) {
        String key = "BLOG_LIKED_KEY" + id;
        Set<String> set = stringRedisTemplate.opsForZSet().range(key, 0, -1);
        Integer likeNumber = 0;
        if (set != null) likeNumber = set.size();
        return likeNumber;
    }//这里的数据持久化就由定时任务去执行吧


    //在redis里查询该博客下所有点赞的用户ids
    @Override
    public List<Long> queryAllBlogLikesUserIds(Long id) {
        String key = "BLOG_LIKED_KEY" + id;
        return queryAllBlogLikesUserIdsByRedisKey(key);
    }

    private List<Long> queryAllBlogLikesUserIdsByRedisKey(String key) {
        Set<String> stringUserList = stringRedisTemplate.opsForZSet().range(key, 0, -1);
        if (stringUserList == null || stringUserList.isEmpty()) return Collections.emptyList();

        List<Long> ids = stringUserList.stream().map(Long::valueOf).collect(Collectors.toList());

        return ids;
    }

    @Autowired
    private UserService userService;

    //在redis里查询该篇博客里所有点赞的用户users
    @Override
    public List<User> getAllBlogLikesUsersByBlogId(Long id) {
        List<Long> ids = queryAllBlogLikesUserIds(id);

        String idStr = StrUtil.join(",", ids);
        // 根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
        List<User> users = new ArrayList<>(userService.query()
                .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")")
                .list());
        return users;
    }

    //将数据库里的likeList拿出来
    @Override
    public HashMap<String, String> getIdsInTableByBlogId(Long id) {
        Blog blog = getById(id);
        String likeList = blog.getLikeList();


        HashMap<String, String> hashMap = (HashMap<String, String>) JSONUtil.toBean(likeList, Map.class);

        return hashMap;
    }

    @Autowired
    private JedisPool jedisPool;


    private Set<String> getAllRedisKeyByKeys(String prefix) {
        Set<String> keys = stringRedisTemplate.keys(prefix.concat("*"));
        return keys;
    }

    @Override
    public Result likeBlog(Long blogId, Long userId) {

        String key = RedisContents.BLOG_LIKED_KEY + blogId;
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());//1-2.查询并返回结果
        long nowTime = System.currentTimeMillis();

        if (score == null) {//3.判断
            //如果未点赞，可以点赞。即保存用户到Redis的set集合
            stringRedisTemplate.opsForZSet().add(key, userId.toString(), nowTime);//3.1写入点赞数据
        } else {
            //如果已点赞，取消点赞。即把用户从Redis的set集合移除
            stringRedisTemplate.opsForZSet().remove(key, userId.toString());//3.2删除该点赞数据
        }
        //4.设置该评论的最新更新时间为当前时间
        stringRedisTemplate.opsForValue().set(RedisContents.BLOG_LIKES_KEY_UPDATE_TIME + blogId, String.valueOf(nowTime));//https://blog.csdn.net/weixin_41585557/article/details/83753958
        return Result.ok();
    }


    @Override
    public Result updateTargetLikeListToDatabaseByThreadPool(String prefix) {
        int totalCount = 0;
        Jedis jedis = null;
        ThreadPoolExecutor updateForDatabasePool = ThreadPoolUtils.getThreadPoolWithCallerRunsPolicy(SystemContents.CPU_COUNT, SystemContents.CPU_COUNT);
        ThreadPoolExecutor getLikeListFromRedisPool = ThreadPoolUtils.getThreadPoolWithCallerRunsPolicy(SystemContents.CPU_COUNT, SystemContents.CPU_COUNT);

        ScanParams scanParams = new ScanParams();
        scanParams.match(prefix.concat("*"));// 匹配以 prefix-* 为前缀的 key
        scanParams.count(RedisContents.SCAN_COUNT);
        String lastBatchUpdateTime = stringRedisTemplate.opsForValue().get(RedisContents.BATCH_UPDATE_TIME);
        if (lastBatchUpdateTime == null) lastBatchUpdateTime = "0";
        Long lastTime = Long.valueOf(lastBatchUpdateTime);
        try {
            jedis = jedisPool.getResource();
            String cursor = "0";
            ArrayList<Future<Boolean>> futures = new ArrayList<>(SystemContents.CPU_COUNT);
            do {
                // 扫描并获取一部分key
                ScanResult<String> result = jedis.scan(cursor, scanParams);//https://www.cnblogs.com/xubiao/p/8489522.html
                // 记录cursor
                cursor = result.getCursor();
                List<String> list = result.getResult();
                totalCount += list.size();
                if (list == null || list.isEmpty()) break;
                for (int i = 0; i < list.size(); i++) {
                    String updateKey = list.get(i);
                    log.debug("key is {}", updateKey);//这里可以看到有哪些key
                    Long blogId = Long.parseLong(updateKey.substring(prefix.length(), updateKey.length()));

                    Future<Boolean> f = updateForDatabasePool.submit(() -> {
                        Future<Map<String, String>> future = getLikeListFromRedisPool.submit(() -> {
                            return queryAllBlogLikesUserIdWithScoresByUpdateKey(updateKey, blogId, lastTime);
                        });

                        Map<String, String> userIdWithTime = null;
                        try {
                            userIdWithTime = future.get();
                        } catch (InterruptedException | ExecutionException e) {
                            e.printStackTrace();
                        }
                        return durableToDatabaseByBlogIdAndMap(userIdWithTime, blogId);
                    });
                    futures.add(f);

                    if (i % SystemContents.CPU_COUNT == SystemContents.CPU_COUNT - 3 || i == list.size() - 1) {
                        futures.forEach(t -> {
                            try {
                                t.get();
                            } catch (InterruptedException | ExecutionException e) {
                                e.printStackTrace();
                            }
                        });
                        futures.clear();
                    }
                }

            } while (!cursor.equals("0"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) jedis.close();
            ThreadPoolUtils.shutdownThreadPool(updateForDatabasePool);
            ThreadPoolUtils.shutdownThreadPool(getLikeListFromRedisPool);
        }
        stringRedisTemplate.opsForValue().set(RedisContents.BATCH_UPDATE_TIME, String.valueOf(System.currentTimeMillis()));//如果宕机了，再这一步没执行成功
        //那么怎么办？应该是重新启动机器后就可以继续同步。


        log.debug("totalCount is {}", totalCount);
        return Result.ok("some like_lists had bean durable in database from cache");
    }


    private Map<String, String> queryAllBlogLikesUserIdWithScoresByUpdateKey(String updateKey, Long blogId, Long lastBatchUpdateTime) {
        String lastUpdateTime = stringRedisTemplate.opsForValue().get(updateKey);
        //如果这篇博客点赞信息的最新更新时间<=上一次定时批量同步的时间,则不需要理会，因为是旧数据。
        if (lastUpdateTime != null && Long.parseLong(lastUpdateTime) <= lastBatchUpdateTime) return null;

//        TODO 看看有没有ZrangeWithScores的方式，这是一个可以优化的点
        String key = RedisContents.BLOG_LIKED_KEY + blogId;
        Set<String> stringUserIdList = stringRedisTemplate.opsForZSet().range(key, 0, -1);

        if (stringUserIdList == null || stringUserIdList.size() == 0) return null;

        HashMap<String, String> userIdWithTime = new HashMap<>(stringUserIdList.size());

        for (String userId : stringUserIdList) {//暂时找不到可以直接返回List<Double>的api
            String score = stringRedisTemplate.opsForZSet().score(key, userId).toString();
            userIdWithTime.put(userId, score);
        }

        stringRedisTemplate.opsForValue().set(updateKey, String.valueOf(System.currentTimeMillis()));//修改这个博客的点赞数据的更新时间
        return userIdWithTime;
    }

    private boolean durableToDatabaseByBlogIdAndMap(Map<String, String> userIdWithTime, Long id) {
        //什么都没有那肯定不操作
        if (userIdWithTime == null || userIdWithTime.size() == 0) return true;

        String stringIds = userIdWithTime.toString();

        String updateSql = "like_list = " + "'" + stringIds + "' ," +
                "like_number = " + userIdWithTime.size();
        return update()
                .setSql(updateSql)
                .eq("id", id)
                .update();
    }


}
